﻿Imports MPQService
Imports System.ServiceModel
Imports System.IO

<ServiceBehavior(ConcurrencyMode:=ConcurrencyMode.Multiple, InstanceContextMode:=InstanceContextMode.Single)> _
Public Class GameServer
    Implements IServer

#Region "Events"
    Event NewGame(ByVal ID As Integer, ByVal Name As String)
    Event UpdateGameStatus(ByVal ID As Integer, ByVal Status As String, ByVal Detail As String)
    Event UpdateGameScores(ByVal ID As Integer, ByVal Scores As SortedList(Of String, Long))
    Event EndGame(ByVal ID As Integer)
    Event GameReset(ByVal ID As Integer)
    Event UpdateServerStatus(ByVal Status As String, ByVal Detail As String)
    Event UserLogin(ByVal Name As String)
    Event UserCreated(ByVal Name As String)
    Event UserLogOut(ByVal Name As String)
#End Region

    Public Games As New Dictionary(Of Integer, Game)
    Public ServerData As New ServerData
    Public LoggedInUsers As New Dictionary(Of IClient, String)
    Public ServerMessages As New List(Of ServerMessage)

    Friend Closing As Boolean = False
    Friend SingleGameOnly As Boolean
    Public ServerPassword As String

    Dim SaveTimer As Threading.Timer
    Dim SaveFile As String
    Public LastSave As Date

    Public Sub InitializeServer(ByVal SingleGame As Boolean, Optional ByVal SingleGameName As String = "", Optional ByVal ServerDataFile As String = "ServerData.mpqserver")
        SingleGameOnly = SingleGame
        If SingleGame Then
            CreateGame(1, SingleGameName)
        End If
        Try
            ServerData.ReadXml(ServerDataFile)
        Catch
        End Try
        SaveFile = ServerDataFile
        SaveTimer = New Threading.Timer(New Threading.TimerCallback(AddressOf Save), Nothing, 120000, 120000)
        ServerData.Usage_Data.AddUsage_DataRow(Now, 0, 0)

        Try
            RaiseEvent UpdateServerStatus("Removing old Usage Data...", "")
            Dim Query = From Usage_Data In ServerData.Usage_Data Where Usage_Data._Date < Now.AddDays(-7)
            Dim RowIndexToRemove As New Queue(Of Integer)
            For Each Row As DataRow In Query
                RowIndexToRemove.Enqueue(ServerData.Usage_Data.Rows.IndexOf(Row))
            Next

            Dim removed As Integer = 0
            Do Until RowIndexToRemove.Count = 0
                ServerData.Usage_Data.Rows.RemoveAt(RowIndexToRemove.Dequeue - removed)
                removed += 1
            Loop

            For Each Row As ServerData.UsersRow In ServerData.Users
                If Row.IsTimeMultiplierNull Then
                    Row.TimeMultiplier = 1
                End If
                If Row.IsScoreNull Then
                    Row.Score = 0
                End If
            Next
        Catch ex As Exception
            RaiseEvent UpdateServerStatus("Failed to remove old usage data: " & ex.Message, "")
        End Try
    End Sub

    Public Sub Save()
        If ServerData.HasChanges Then
            ServerData.AcceptChanges()
            ServerData.WriteXml(SaveFile)
            LastSave = Now
        End If
    End Sub

    Public Sub CloseServer()
        Closing = True
        Save()
    End Sub

    Public Sub CreateGame(ByVal id As Integer, ByVal Name As String) Implements IServer.CreateGame
        Name = Name.Replace("(", "[")
        Name = Name.Replace(")", "]")

        Try
            Dim Game As Game = New Game(Name, id, Me)
            Games.Add(id, Game)
            Try
                GameNames.Add(Name, id)
                RaiseEvent NewGame(id, Name)
            Catch ex As Exception
                If ex.Message = "An item with the same key has already been added." Then
                    Dim i As Integer = 2
                    Do
                        Try
                            GameNames.Add(Name & " [" & i & "]", id)
                            Game.Name = Name & " [" & i & "]"
                            Try
                                RaiseEvent NewGame(id, Name & " [" & i & "]")
                            Catch
                            End Try
                            Exit Do
                        Catch ex2 As Exception
                            If ex2.Message = "An item with the same key has already been added." Then
                                i += 1
                            Else
                                RaiseEvent UpdateServerStatus("Error creating new game: " & ex2.Message, "")
                                Exit Do
                            End If
                        End Try
                    Loop
                Else
                    RaiseEvent UpdateServerStatus("Error creating new game: " & ex.Message, "")
                End If
            End Try
            AddHandler Game.UpdateGameStatus, AddressOf _UpdateGameStatus
            AddHandler Game.UpdateGameScores, AddressOf _UpdateGameScoreboard
            AddHandler Game.EndGame, AddressOf _EndGame
            AddHandler Game.GameReset, AddressOf _GameReset
        Catch
        End Try
    End Sub

    Public Sub _UpdateGameScoreboard(ByVal ID As Integer, ByVal Scores As SortedList(Of String, Long))
        RaiseEvent UpdateGameScores(ID, Scores)
    End Sub

    Public Sub _UpdateGameStatus(ByVal ID As Integer, ByVal Status As String, ByVal Detail As String)
        RaiseEvent UpdateGameStatus(ID, Status, Detail)
    End Sub

    Public Sub _EndGame(ByVal ID As Integer)
        RaiseEvent EndGame(ID)
    End Sub

    Public Sub _GameReset(ByVal ID As Integer)
        RaiseEvent GameReset(ID)
    End Sub

    Public GameNames As New Dictionary(Of String, Integer)
    Public Sub GetGames() Implements IServer.GetGames
        OperationContext.Current.GetCallbackChannel(Of IClient).OnGetGames(GameNames)
    End Sub

    Dim UploadingQuizzes As New Dictionary(Of String, Dictionary(Of Integer, String))

    Public Sub UploadQuizFile(ByVal QuizFile As String, ByVal Name As String, ByVal PartNo As Integer, ByVal TotalParts As Integer) Implements IServer.UploadQuizFile
        If Not UploadingQuizzes.ContainsKey(Name) Then
            UploadingQuizzes.Add(Name, New Dictionary(Of Integer, String))
        End If
        UploadingQuizzes(Name).Add(PartNo, QuizFile)
        If Not TotalParts = 0 Then
            Dim PercentComplete As Integer = PartNo / TotalParts
            Try
                RaiseEvent UpdateServerStatus(LoggedInUsers(OperationContext.Current.GetCallbackChannel(Of IClient)) & " is uploading " & Name, PercentComplete & "%")
            Catch
            End Try
        End If
        If PartNo = TotalParts Then
            Dim WQFThread As New Threading.Thread(AddressOf WriteQuizFile)
            WQFThread.Start({Name, OperationContext.Current.GetCallbackChannel(Of IClient)()})
            RaiseEvent UpdateServerStatus(LoggedInUsers(OperationContext.Current.GetCallbackChannel(Of IClient)) & " is uploading " & Name, "Complete")
        End If
    End Sub

    Sub WriteQuizFile(ByVal Params())
        Dim Name As String = Params(0)
        Dim Context As IClient = Params(1)

        Dim Quiz As String = ""

        For i = 0 To UploadingQuizzes(Name).Count - 1
            Quiz += UploadingQuizzes(Name)(i)
        Next

        Dim stream As New IO.FileStream("Quiz Files\" & Name, IO.FileMode.Create)
        Dim comp As New System.IO.Compression.GZipStream(stream, IO.Compression.CompressionMode.Compress)
        Dim SW As New IO.StreamWriter(comp)
        SW.Write(Quiz)
        SW.Close()
        comp.Close()
        stream.Close()

        UploadingQuizzes.Remove(Name)
        Context.OnGetQuizFiles(IO.Directory.GetFiles("Quiz Files\"))
    End Sub

    Public Sub GetQuizFiles() Implements IServer.GetQuizFiles
        OperationContext.Current.GetCallbackChannel(Of IClient).OnGetQuizFiles(IO.Directory.GetFiles("Quiz Files\"))
    End Sub

    Public Sub ReadyToStart(ByVal GameID As Integer, ByVal ID As Integer) Implements IServer.ReadyToStart
        Games.Item(GameID).ReadyToStart(ID)
    End Sub

    Public Sub InputAnswer(ByVal GameID As Integer, ByVal Answer As String, ByVal ID As Integer) Implements IServer.InputAnswer
        Games.Item(GameID).RecieveAnswer(Answer, ID)
    End Sub

    Public Sub MultichoiceAnswer(ByVal GameID As Integer, ByVal Answer As Answer, ByVal ID As Integer) Implements IServer.MultichoiceAnswer
        Games.Item(GameID).RecieveAnswer(Answer, ID)
    End Sub

    Public Sub OutOfTime(ByVal GameID As Integer, ByVal ID As Integer) Implements IServer.OutOfTime
        Games.Item(GameID).OutOfTime(ID)
    End Sub

    Public Sub RegisterClient(ByVal GameID As Integer) Implements IServer.RegisterClient
        Try
            Games.Item(GameID).RegisterClient(OperationContext.Current.GetCallbackChannel(Of IClient))
        Catch ex As Exception
            OperationContext.Current.GetCallbackChannel(Of IClient).OnFailRegisterClient(ex.Message)
        End Try
    End Sub

    Public Sub RegisterHost(ByVal GameID As Integer, ByVal Name As String, ByVal QuizFile As String) Implements IServer.RegisterHost
        Games.Item(GameID).RegisterHost(Name, QuizFile, OperationContext.Current.GetCallbackChannel(Of IClient))
    End Sub

    Public Sub RegisterUser(ByVal GameID As Integer, ByVal Name As String) Implements IServer.RegisterUser
        Try
            Games.Item(GameID).RegisterUser(Name, OperationContext.Current.GetCallbackChannel(Of IClient))
        Catch
        End Try
    End Sub

    Public Sub StartQuiz(ByVal GameID As Integer) Implements IServer.StartQuiz
        Games.Item(GameID).StartQuiz()
    End Sub

    Public Sub UnregisterClient(ByVal GameID As Integer) Implements IServer.UnregisterClient
        Games.Item(GameID).UnregisterClient(OperationContext.Current.GetCallbackChannel(Of IClient))
    End Sub

    Public Sub UnregisterUser(ByVal GameID As Integer, ByVal ID As Integer) Implements IServer.UnregisterUser
        Try
            Games.Item(GameID).UnregisterUser(ID, OperationContext.Current.GetCallbackChannel(Of IClient))
        Catch ex As Exception
            Try
                RaiseEvent UpdateServerStatus("Error unregistering a user in " & GameNames(GameID) & ":" & ex.Message, "")
            Catch
                RaiseEvent UpdateServerStatus("Error unregistering a user:" & ex.Message, "")
            End Try
        End Try
    End Sub

    Public Sub GetGameDetails(ByVal GameID As Integer) Implements IServer.GetGameDetails
        Games(GameID).GetGameDetails(OperationContext.Current.GetCallbackChannel(Of IClient))
    End Sub

    Public Sub DoLogin(ByVal Username As String, ByVal Password As String) Implements IServer.DoLogin
        Dim Results As EnumerableRowCollection(Of ServerData.UsersRow) = From Users In ServerData.Users Where Users.Username = Username

        Dim sha1 As System.Security.Cryptography.SHA1 = System.Security.Cryptography.SHA1.Create()
        Dim hashData As Byte() = sha1.ComputeHash(Text.Encoding.[Default].GetBytes(Password))

        Password = Convert.ToBase64String(hashData)

        Try
            If Results(0).Password = Password Then
                OperationContext.Current.GetCallbackChannel(Of IClient).OnLoginSuccess()
                LoggedInUsers.Add(OperationContext.Current.GetCallbackChannel(Of IClient), Username)
                RaiseEvent UserLogin(Username)
            Else
                OperationContext.Current.GetCallbackChannel(Of IClient).OnLoginFail()
            End If
        Catch
            OperationContext.Current.GetCallbackChannel(Of IClient).OnLoginFail()
        End Try
    End Sub

    Public Sub CreateUser(ByVal Username As String, ByVal Password As String) Implements IServer.CreateUser

        Dim sha1 As System.Security.Cryptography.SHA1 = System.Security.Cryptography.SHA1.Create()
        Dim hashData As Byte() = sha1.ComputeHash(Text.Encoding.[Default].GetBytes(Password))

        Password = Convert.ToBase64String(hashData)

        Try
            ServerData.Users.AddUsersRow(Username, Password, 0, Nothing, 1).SetTeamNull()
            OperationContext.Current.GetCallbackChannel(Of IClient).OnLoginSuccess()
            LoggedInUsers.Add(OperationContext.Current.GetCallbackChannel(Of IClient), Username)
            RaiseEvent UserCreated(Username)
        Catch ex As Exception
            If ServerData.Users.Rows.Contains(Username) Then
                OperationContext.Current.GetCallbackChannel(Of IClient).OnCreateUserFail("The selected username already exists")
            Else
                OperationContext.Current.GetCallbackChannel(Of IClient).OnCreateUserFail(ex.Message)
            End If
        End Try
    End Sub

    Public Sub ChangePassword(ByVal Username As String, ByVal OldPassword As String, ByVal NewPassword As String) Implements IServer.ChangePassword
        Dim Results As EnumerableRowCollection(Of ServerData.UsersRow) = From Users In ServerData.Users Where Users.Username = Username

        Dim sha1 As System.Security.Cryptography.SHA1 = System.Security.Cryptography.SHA1.Create()
        Dim hashData As Byte() = sha1.ComputeHash(Text.Encoding.[Default].GetBytes(OldPassword))

        OldPassword = Convert.ToBase64String(hashData)

        Try
            If Results(0).Password = OldPassword Then
                Dim nsha1 As System.Security.Cryptography.SHA1 = System.Security.Cryptography.SHA1.Create()
                Dim nhashData As Byte() = nsha1.ComputeHash(Text.Encoding.[Default].GetBytes(NewPassword))

                NewPassword = Convert.ToBase64String(nhashData)

                Results(0).Password = NewPassword

                OperationContext.Current.GetCallbackChannel(Of IClient).OnChangePassword(True)
            Else
                OperationContext.Current.GetCallbackChannel(Of IClient).OnChangePassword(False, New Exception("Incorrect Password"))
            End If
        Catch ex As Exception
            OperationContext.Current.GetCallbackChannel(Of IClient).OnChangePassword(False, ex)
        End Try
    End Sub

    Public Sub GetUserDetails(ByVal Username As String) Implements IServer.GetUserDetails
        Dim Results As EnumerableRowCollection(Of ServerData.UsersRow) = From Users In ServerData.Users Where Users.Username = Username
        Try
            OperationContext.Current.GetCallbackChannel(Of IClient).OnGetUserDetails(Results(0).Score, Results(0).Team, Results(0).TimeMultiplier)
        Catch
            OperationContext.Current.GetCallbackChannel(Of IClient).OnGetUserDetails(Results(0).Score, "No Team", Results(0).TimeMultiplier)
        End Try
    End Sub

    Shared UserScores As New Collections.Generic.SortedList(Of String, Long)(New UserScoresSorter)
    Sub _UpdateUserScores()
        Dim _UserScores As New SortedList(Of String, Long)
        For Each User As ServerData.UsersRow In ServerData.Users
            _UserScores.Add(User.Username, User.Score)
        Next
        UserScores = _UserScores
    End Sub

    Class UserScoresSorter
        Implements IComparer(Of String)

        Public Function Compare(ByVal x As String, ByVal y As String) As Integer Implements System.Collections.Generic.IComparer(Of String).Compare
            Try
                Dim returnVal As Integer = -1
                returnVal = 1 - Decimal.Compare(UserScores(x), UserScores(y))
                Return returnVal
            Catch
                Return 0
            End Try
        End Function
    End Class

    Shared TeamScores As New SortedList(Of String, Long)(New TeamScoresSorter)
    Sub _UpdateTeamScores()
        Dim _TeamScores As New SortedList(Of String, Long)
        For Each Team As ServerData.TeamsRow In ServerData.Teams
            _TeamScores.Add(Team.Name, Team.Score)
        Next
        TeamScores = _TeamScores
    End Sub

    Class TeamScoresSorter
        Implements IComparer(Of String)

        Public Function Compare(ByVal x As String, ByVal y As String) As Integer Implements System.Collections.Generic.IComparer(Of String).Compare
            Try
                Dim returnVal As Integer = -1
                returnVal = 1 - Decimal.Compare(TeamScores(x), TeamScores(y))
                Return returnVal
            Catch
                Return 0
            End Try
        End Function
    End Class

    Public Sub UpdateTeamScores() Implements IServer.UpdateTeamScores
        _UpdateTeamScores()
        OperationContext.Current.GetCallbackChannel(Of IClient).OnUpdateTeamScores(TeamScores)
    End Sub

    Public Sub UpdateUserScores() Implements IServer.UpdateUserScores
        _UpdateUserScores()
        OperationContext.Current.GetCallbackChannel(Of IClient).OnUpdateUserScores(UserScores)
    End Sub

    Public Sub CreateTeam(ByVal Teamname As String, ByVal User As String) Implements IServer.CreateTeam
        Dim Results As EnumerableRowCollection(Of ServerData.UsersRow) = From Users In ServerData.Users Where Users.Username = User
        Results(0).TeamsRow = ServerData.Teams.AddTeamsRow(Teamname, 0)
        RaiseEvent UpdateServerStatus(LoggedInUsers(OperationContext.Current.GetCallbackChannel(Of IClient)) & " has created team " & Teamname, "")
    End Sub

    Public Sub JoinTeam(ByVal Teamname As String, ByVal Username As String) Implements IServer.JoinTeam
        Dim Users As EnumerableRowCollection(Of ServerData.UsersRow) = From User In ServerData.Users Where User.Username = Username
        Dim Teams As EnumerableRowCollection(Of ServerData.TeamsRow) = From Team In ServerData.Teams Where Team.Name = Teamname
        Users(0).TeamsRow = Teams(0)
        RaiseEvent UpdateServerStatus(LoggedInUsers(OperationContext.Current.GetCallbackChannel(Of IClient)) & " has joined team " & Teamname, "")
    End Sub

    Public Sub GetHaveHost(ByVal GameID As Integer) Implements IServer.GetHaveHost
        Games(GameID).GetHaveHost(OperationContext.Current.GetCallbackChannel(Of IClient))
    End Sub

    Public Sub Goodbye() Implements IServer.Goodbye
        Try
            RaiseEvent UserLogOut(LoggedInUsers(OperationContext.Current.GetCallbackChannel(Of IClient)))
            LoggedInUsers.Remove(OperationContext.Current.GetCallbackChannel(Of IClient))
        Catch
        End Try
    End Sub

    Public Sub Hello() Implements IServer.Hello
        Dim RequiresPassword As Boolean = False
        If Not ServerPassword = "" Then RequiresPassword = True
        OperationContext.Current.GetCallbackChannel(Of IClient).OnHello(SingleGameOnly, ServerMessages, RequiresPassword)
    End Sub

    Public Sub IncorrectPassword() Implements IServer.IncorrectPassword
        OperationContext.Current.GetCallbackChannel(Of IClient).OnIncorrectServerPassword()
    End Sub

    Public Sub SetUserMultiplier(ByVal Username As String, ByVal Multiplier As Double) Implements IServer.SetUserMultipler
        Dim Results As EnumerableRowCollection(Of ServerData.UsersRow) = From Users In ServerData.Users Where Users.Username = Username
        If Multiplier > 4 Then Multiplier = 4
        If Multiplier = 0 Then Multiplier = 0.2
        Results(0).TimeMultiplier = Multiplier
    End Sub

    Public Class ClientBehavior
        Implements Description.IEndpointBehavior
        Implements Dispatcher.IInstanceContextInitializer
        Implements Dispatcher.IDispatchOperationSelector

        Dim GS As GameServer
        Dim IC As InstanceContext
        Dim Timer As Threading.Timer

        Event SetStatus(ByVal Status As String)

        Sub New(ByVal GameServer As GameServer)
            GS = GameServer
        End Sub

#Region "Unused Endpoint Behavior Implementations"
        Public Sub AddBindingParameters(ByVal endpoint As System.ServiceModel.Description.ServiceEndpoint, ByVal bindingParameters As System.ServiceModel.Channels.BindingParameterCollection) Implements System.ServiceModel.Description.IEndpointBehavior.AddBindingParameters

        End Sub

        Public Sub ApplyClientBehavior(ByVal endpoint As System.ServiceModel.Description.ServiceEndpoint, ByVal clientRuntime As System.ServiceModel.Dispatcher.ClientRuntime) Implements System.ServiceModel.Description.IEndpointBehavior.ApplyClientBehavior

        End Sub

        Public Sub Validate(ByVal endpoint As System.ServiceModel.Description.ServiceEndpoint) Implements System.ServiceModel.Description.IEndpointBehavior.Validate

        End Sub
#End Region

        Public Sub ApplyDispatchBehavior(ByVal endpoint As System.ServiceModel.Description.ServiceEndpoint, ByVal endpointDispatcher As System.ServiceModel.Dispatcher.EndpointDispatcher) Implements System.ServiceModel.Description.IEndpointBehavior.ApplyDispatchBehavior
            endpointDispatcher.DispatchRuntime.InstanceContextInitializers.Add(Me)
            endpointDispatcher.DispatchRuntime.OperationSelector = Me
        End Sub

        Public Sub Initialize(ByVal instanceContext As System.ServiceModel.InstanceContext, ByVal message As System.ServiceModel.Channels.Message) Implements System.ServiceModel.Dispatcher.IInstanceContextInitializer.Initialize
            IC = instanceContext
            Timer = New Threading.Timer(New Threading.TimerCallback(AddressOf Watcher), Nothing, 5000, 5000)
        End Sub

        Dim LastCount As Integer
        Sub Watcher()
            Try
                For Each Game As KeyValuePair(Of Integer, Game) In GS.Games
                    For Each User As User In Game.Value.Users.ToArray
                        If Not IC.IncomingChannels.Contains(User.Channel) Then
                            Game.Value._UnregisterUser(User.ID)
                            Game.Value._UnregisterClient(User.CallBack)
                        End If
                        If User.Channel.State = CommunicationState.Faulted Then
                            Game.Value._UnregisterUser(User.ID)
                            Game.Value._UnregisterClient(User.CallBack)
                        End If
                    Next
                Next
            Catch ex As Exception
                RaiseEvent SetStatus("Client Watcher: " & ex.Message)
            End Try

            If Not IC.IncomingChannels.Count = LastCount Then
                Try
                    With GS.ServerData.Usage_Data
                        .AddUsage_DataRow(Now, IC.IncomingChannels.Count, .Last().DisplayCount)
                    End With
                Catch
                End Try
                LastCount = IC.IncomingChannels.Count
            End If
        End Sub

        Public Function SelectOperation(ByRef message As System.ServiceModel.Channels.Message) As String Implements System.ServiceModel.Dispatcher.IDispatchOperationSelector.SelectOperation
            If Not GS.ServerPassword = "" Then
                Try
                    If message.Headers.Action.Contains("Hello") Then Exit Try
                    'If Not request.Headers.GetHeader(Of Boolean)("KnowNeedPassword", "ns") Then Exit Try
                    If Not message.Headers.GetHeader(Of String)("ServerPassword", "ns") = GS.ServerPassword Then
                        Throw New Exception("Incorrect Password")
                    End If
                Catch
                    Dim MS As New IO.MemoryStream
                    Dim SW As New IO.StreamWriter(MS)
                    SW.WriteLine("<IncorrectPassword xmlns=" & """"c & "http://tempuri.org/" & """"c & "></IncorrectPassword>")
                    SW.Flush()

                    MS.Position = 0
                    Dim XR As Xml.XmlReader = Xml.XmlReader.Create(MS)

                    message = Channels.Message.CreateMessage(message.Version, Nothing, XR)
                    Return "IncorrectPassword"
                End Try
            End If

            Return message.Headers.Action.Replace("http://tempuri.org/IServer/", "")
        End Function
    End Class
End Class
